From cef149a6afd4e70465700db0825bd4e7ca14e4ff Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jonas=20Dre=C3=9Fler?= Date: Fri, 18 Mar 2022 16:25:38 +0100 Subject: [PATCH] Port the EdgeDragAction to ClutterGesture And since ClutterGestures will only be possible to implement in C (due to performance concerns when we'll get rid of motion event compression and start handling eg. 1000 HZ mouse events at the full rate), that means we have to port the gesture to C! It behaves mostly the same still: It's a single finger touch-only gesture that recognizes as soon as a point that began on the monitor edge moves away from that edge. It then emits "progress" signals with the distance from the edge all the way until the fixed threshold distance (DRAG_DISTANCE) is reached, where it then moves to COMPLETED. There's definitely room for further improvements here, eg. the gesture should probably only complete once the point is lifted from the screen. Part-of: --- js/js-resources.gresource.xml | 1 - js/ui/edgeDragAction.js | 87 -------- js/ui/keyboard.js | 34 +-- js/ui/windowManager.js | 17 +- src/meson.build | 2 + src/shell-edge-drag-gesture.c | 394 ++++++++++++++++++++++++++++++++++ src/shell-edge-drag-gesture.h | 34 +++ 7 files changed, 459 insertions(+), 110 deletions(-) delete mode 100644 js/ui/edgeDragAction.js create mode 100644 src/shell-edge-drag-gesture.c create mode 100644 src/shell-edge-drag-gesture.h diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index dc182cc74..e5e6167f1 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -65,7 +65,6 @@ ui/dateMenu.js ui/dialog.js ui/dnd.js - ui/edgeDragAction.js ui/endSessionDialog.js ui/environment.js ui/extensionDownloader.js diff --git a/js/ui/edgeDragAction.js b/js/ui/edgeDragAction.js deleted file mode 100644 index 99a3711de..000000000 --- a/js/ui/edgeDragAction.js +++ /dev/null @@ -1,87 +0,0 @@ -import Clutter from 'gi://Clutter'; -import GObject from 'gi://GObject'; -import Mtk from 'gi://Mtk'; -import St from 'gi://St'; - -import * as Main from './main.js'; - -const EDGE_THRESHOLD = 20; -const DRAG_DISTANCE = 80; - -export const EdgeDragAction = GObject.registerClass({ - Signals: { - 'activated': {}, - 'progress': {param_types: [GObject.TYPE_DOUBLE]}, - }, -}, class EdgeDragAction extends Clutter.GestureAction { - _init(side, allowedModes) { - super._init(); - this._side = side; - this._allowedModes = allowedModes; - this.set_n_touch_points(1); - this.set_threshold_trigger_edge(Clutter.GestureTriggerEdge.AFTER); - } - - _getMonitorRect(x, y) { - const rect = new Mtk.Rectangle({x: x - 1, y: y - 1, width: 1, height: 1}); - let monitorIndex = global.display.get_monitor_index_for_rect(rect); - - return global.display.get_monitor_geometry(monitorIndex); - } - - vfunc_gesture_prepare(_actor) { - if (this.get_n_current_points() === 0) - return false; - - if (!(this._allowedModes & Main.actionMode)) - return false; - - let [x, y] = this.get_press_coords(0); - let monitorRect = this._getMonitorRect(x, y); - - return (this._side === St.Side.LEFT && x < monitorRect.x + EDGE_THRESHOLD) || - (this._side === St.Side.RIGHT && x > monitorRect.x + monitorRect.width - EDGE_THRESHOLD) || - (this._side === St.Side.TOP && y < monitorRect.y + EDGE_THRESHOLD) || - (this._side === St.Side.BOTTOM && y > monitorRect.y + monitorRect.height - EDGE_THRESHOLD); - } - - vfunc_gesture_progress(_actor) { - let [startX, startY] = this.get_press_coords(0); - let [x, y] = this.get_motion_coords(0); - let offsetX = Math.abs(x - startX); - let offsetY = Math.abs(y - startY); - - if (offsetX < EDGE_THRESHOLD && offsetY < EDGE_THRESHOLD) - return true; - - if ((offsetX > offsetY && - (this._side === St.Side.TOP || this._side === St.Side.BOTTOM)) || - (offsetY > offsetX && - (this._side === St.Side.LEFT || this._side === St.Side.RIGHT))) { - this.cancel(); - return false; - } - - if (this._side === St.Side.TOP || - this._side === St.Side.BOTTOM) - this.emit('progress', offsetY); - else - this.emit('progress', offsetX); - - return true; - } - - vfunc_gesture_end(_actor) { - let [startX, startY] = this.get_press_coords(0); - let [x, y] = this.get_motion_coords(0); - let monitorRect = this._getMonitorRect(startX, startY); - - if ((this._side === St.Side.TOP && y > monitorRect.y + DRAG_DISTANCE) || - (this._side === St.Side.BOTTOM && y < monitorRect.y + monitorRect.height - DRAG_DISTANCE) || - (this._side === St.Side.LEFT && x > monitorRect.x + DRAG_DISTANCE) || - (this._side === St.Side.RIGHT && x < monitorRect.x + monitorRect.width - DRAG_DISTANCE)) - this.emit('activated'); - else - this.cancel(); - } -}); diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js index e3c8cd07a..47f0b00b3 100644 --- a/js/ui/keyboard.js +++ b/js/ui/keyboard.js @@ -9,7 +9,6 @@ import Shell from 'gi://Shell'; import St from 'gi://St'; import * as Signals from '../misc/signals.js'; -import * as EdgeDragAction from './edgeDragAction.js'; import * as InputSourceManager from './status/keyboard.js'; import * as IBusManager from '../misc/ibusManager.js'; import * as BoxPointer from './boxpointer.js'; @@ -1076,22 +1075,25 @@ export class KeyboardManager extends Signals.EventEmitter { this._syncEnabled(); }); - const mode = Shell.ActionMode.ALL & ~Shell.ActionMode.LOCK_SCREEN; - const bottomDragAction = new EdgeDragAction.EdgeDragAction(St.Side.BOTTOM, mode); - bottomDragAction.connect('activated', () => { - if (this._keyboard) - this._keyboard.gestureActivate(Main.layoutManager.bottomIndex); + const allowedModes = Shell.ActionMode.ALL & ~Shell.ActionMode.LOCK_SCREEN; + const bottomDragGesture = new Shell.EdgeDragGesture({ + name: 'OSK show bottom drag', + side: St.Side.BOTTOM, }); - bottomDragAction.connect('progress', (_action, progress) => { - if (this._keyboard) - this._keyboard.gestureProgress(progress); + bottomDragGesture.connect('may-recognize', () => { + return allowedModes & Main.actionMode; }); - bottomDragAction.connect('gesture-cancel', () => { - if (this._keyboard) - this._keyboard.gestureCancel(); + bottomDragGesture.connect('progress', (_action, progress) => { + this._keyboard?.gestureProgress(progress); }); - global.stage.add_action_full('osk', Clutter.EventPhase.CAPTURE, bottomDragAction); - this._bottomDragAction = bottomDragAction; + bottomDragGesture.connect('end', () => { + this._keyboard?.gestureActivate(Main.layoutManager.bottomIndex); + }); + bottomDragGesture.connect('cancel', () => { + this._keyboard?.gestureCancel(); + }); + global.stage.add_action(bottomDragGesture); + this._bottomDragGesture = bottomDragGesture; this._syncEnabled(); } @@ -1116,13 +1118,13 @@ export class KeyboardManager extends Signals.EventEmitter { this._keyboard = new Keyboard(); this._keyboard.connect('visibility-changed', () => { this.emit('visibility-changed'); - this._bottomDragAction.enabled = !this._keyboard.visible; + this._bottomDragGesture.enabled = !this._keyboard.visible; }); } else if (!enabled && this._keyboard) { this._keyboard.setCursorLocation(null); this._keyboard.destroy(); this._keyboard = null; - this._bottomDragAction.enabled = true; + this._bottomDragGesture.enabled = true; } } diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index a9936c530..d654228fc 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -16,7 +16,6 @@ import * as InhibitShortcutsDialog from './inhibitShortcutsDialog.js'; import * as ModalDialog from './modalDialog.js'; import * as WindowMenu from './windowMenu.js'; import * as PadOsd from './padOsd.js'; -import * as EdgeDragAction from './edgeDragAction.js'; import * as CloseDialog from './closeDialog.js'; import * as SwitchMonitor from './switchMonitor.js'; import * as IBusManager from '../misc/ibusManager.js'; @@ -911,9 +910,15 @@ export class WindowManager { if (Main.sessionMode.hasWorkspaces) this._workspaceTracker = new WorkspaceTracker(this); - let mode = Shell.ActionMode.NORMAL; - let topDragAction = new EdgeDragAction.EdgeDragAction(St.Side.TOP, mode); - topDragAction.connect('activated', () => { + const allowedModes = Shell.ActionMode.NORMAL; + const topDragGesture = new Shell.EdgeDragGesture({ + name: 'Window unfullscreen top drag', + side: St.Side.TOP, + }); + topDragGesture.connect('may-recognize', () => { + return allowedModes & Main.actionMode; + }); + topDragGesture.connect('end', () => { let currentWindow = global.display.focus_window; if (currentWindow) currentWindow.unmake_fullscreen(); @@ -921,14 +926,14 @@ export class WindowManager { let updateUnfullscreenGesture = () => { let currentWindow = global.display.focus_window; - topDragAction.enabled = currentWindow && currentWindow.is_fullscreen(); + topDragGesture.enabled = currentWindow && currentWindow.is_fullscreen(); }; global.display.connect('notify::focus-window', updateUnfullscreenGesture); global.display.connect('in-fullscreen-changed', updateUnfullscreenGesture); updateUnfullscreenGesture(); - global.stage.add_action_full('unfullscreen', Clutter.EventPhase.CAPTURE, topDragAction); + global.stage.add_action(topDragGesture); this._workspaceAnimation = new WorkspaceAnimation.WorkspaceAnimationController(); diff --git a/src/meson.build b/src/meson.build index e16757a40..b729ec03a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -113,6 +113,7 @@ libshell_public_headers = [ 'shell-app-usage.h', 'shell-blur-effect.h', 'shell-camera-monitor.h', + 'shell-edge-drag-gesture.h', 'shell-glsl-effect.h', 'shell-global.h', 'shell-invert-lightness-effect.h', @@ -164,6 +165,7 @@ libshell_sources = [ 'shell-app-usage.c', 'shell-blur-effect.c', 'shell-camera-monitor.c', + 'shell-edge-drag-gesture.c', 'shell-global.c', 'shell-glsl-effect.c', 'shell-invert-lightness-effect.c', diff --git a/src/shell-edge-drag-gesture.c b/src/shell-edge-drag-gesture.c new file mode 100644 index 000000000..a2a84fd2f --- /dev/null +++ b/src/shell-edge-drag-gesture.c @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2025 Jonas Dreßler + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include + +#include "shell-edge-drag-gesture.h" +#include "shell-global.h" + +#define EDGE_THRESHOLD 20 +#define DRAG_DISTANCE 80 + +struct _ShellEdgeDragGesture +{ + ClutterGesture parent; + + StSide side; +}; + +enum +{ + PROP_0, + + PROP_SIDE, + + PROP_LAST +}; + +enum +{ + PROGRESS, + + LAST_SIGNAL +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; +static unsigned int obj_signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_FINAL_TYPE (ShellEdgeDragGesture, shell_edge_drag_gesture, CLUTTER_TYPE_GESTURE); + +static gboolean +get_monitor_for_coords (ShellEdgeDragGesture *self, + graphene_point_t *coords, + MtkRectangle *monitor_geometry_out) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + unsigned int monitor_index; + MtkRectangle rect; + + rect.x = coords->x - 1; + rect.y = coords->y - 1; + rect.width = 1; + rect.height = 1; + + monitor_index = meta_display_get_monitor_index_for_rect (display, &rect); + if (monitor_index == -1) + return FALSE; + + meta_display_get_monitor_geometry (display, monitor_index, monitor_geometry_out); + return TRUE; +} + +static gboolean +is_near_monitor_edge (ShellEdgeDragGesture *self, + unsigned int point) +{ + graphene_point_t coords; + MtkRectangle monitor_geometry; + + clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self), + point, + &coords); + + if (!get_monitor_for_coords (self, &coords, &monitor_geometry)) + { + g_assert_not_reached (); + return FALSE; + } + + switch (self->side) + { + case ST_SIDE_LEFT: + return coords.x < (float) (monitor_geometry.x + EDGE_THRESHOLD); + case ST_SIDE_RIGHT: + return coords.x > (float) (monitor_geometry.x + monitor_geometry.width - EDGE_THRESHOLD); + case ST_SIDE_TOP: + return coords.y < (float) (monitor_geometry.y + EDGE_THRESHOLD); + case ST_SIDE_BOTTOM: + return coords.y > (float) (monitor_geometry.y + monitor_geometry.height - EDGE_THRESHOLD); + } + + g_assert_not_reached (); + return FALSE; +} + +static gboolean +exceeds_cancel_threshold (ShellEdgeDragGesture *self, + unsigned int point) +{ + graphene_point_t begin_coords, latest_coords; + float distance_x, distance_y; + + clutter_gesture_get_point_begin_coords_abs (CLUTTER_GESTURE (self), + point, + &begin_coords); + + clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self), + point, + &latest_coords); + + graphene_point_distance (&latest_coords, &begin_coords, &distance_x, &distance_y); + + switch (self->side) + { + case ST_SIDE_LEFT: + case ST_SIDE_RIGHT: + return distance_y > distance_x; + + case ST_SIDE_TOP: + case ST_SIDE_BOTTOM: + return distance_x > distance_y; + } + + g_assert_not_reached (); + return FALSE; +} + +static gboolean +passes_distance_needed (ShellEdgeDragGesture *self, + unsigned int sequence) +{ + graphene_point_t begin_coords, latest_coords; + MtkRectangle monitor_geometry; + + clutter_gesture_get_point_begin_coords_abs (CLUTTER_GESTURE (self), + sequence, + &begin_coords); + + if (!get_monitor_for_coords (self, &begin_coords, &monitor_geometry)) + { + g_assert_not_reached (); + return FALSE; + } + + clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self), + sequence, + &latest_coords); + + switch (self->side) + { + case ST_SIDE_LEFT: + return latest_coords.x > (float) (monitor_geometry.x + DRAG_DISTANCE); + case ST_SIDE_RIGHT: + return latest_coords.x < (float) (monitor_geometry.x + monitor_geometry.width - DRAG_DISTANCE); + case ST_SIDE_TOP: + return latest_coords.y > (float) (monitor_geometry.y + DRAG_DISTANCE); + case ST_SIDE_BOTTOM: + return latest_coords.y < (float) (monitor_geometry.y + monitor_geometry.height - DRAG_DISTANCE); + } + + g_assert_not_reached (); + return FALSE; +} + +static gboolean +shell_edge_drag_gesture_should_handle_sequence (ClutterGesture *gesture, + const ClutterEvent *sequence_begin_event) +{ + ClutterEventType event_type = clutter_event_type (sequence_begin_event); + + if (event_type == CLUTTER_TOUCH_BEGIN) + return TRUE; + + return FALSE; +} + +static void +shell_edge_drag_gesture_point_began (ClutterGesture *gesture, + unsigned int point) +{ + ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gesture); + unsigned int n_points = clutter_gesture_get_n_points (gesture); + + if (n_points > 1 || + !is_near_monitor_edge (self, point)) + { + clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED); + return; + } +} + +static void +shell_edge_drag_gesture_point_moved (ClutterGesture *gesture, + unsigned int point) +{ + ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gesture); + + if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_POSSIBLE && + is_near_monitor_edge (self, point)) + return; + + if (exceeds_cancel_threshold (self, point)) + { + clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED); + return; + } + + if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_POSSIBLE) + clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_RECOGNIZING); + + if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_RECOGNIZING) + { + graphene_point_t begin_coords, latest_coords; + float distance_x, distance_y; + + clutter_gesture_get_point_begin_coords_abs (gesture, + point, + &begin_coords); + + clutter_gesture_get_point_coords_abs (gesture, + point, + &latest_coords); + + graphene_point_distance (&latest_coords, &begin_coords, &distance_x, &distance_y); + + switch (self->side) + { + case ST_SIDE_LEFT: + case ST_SIDE_RIGHT: + g_signal_emit (self, obj_signals[PROGRESS], 0, distance_x); + break; + + case ST_SIDE_TOP: + case ST_SIDE_BOTTOM: + g_signal_emit (self, obj_signals[PROGRESS], 0, distance_y); + break; + } + + if (passes_distance_needed (self, point)) + clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_COMPLETED); + } +} + +static void +shell_edge_drag_gesture_point_ended (ClutterGesture *gesture, + unsigned int point) +{ + clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED); +} + +static void +shell_edge_drag_gesture_set_property (GObject *gobject, + unsigned int prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gobject); + + switch (prop_id) + { + case PROP_SIDE: + shell_edge_drag_gesture_set_side (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_edge_drag_gesture_get_property (GObject *gobject, + unsigned int prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gobject); + + switch (prop_id) + { + case PROP_SIDE: + g_value_set_enum (value, shell_edge_drag_gesture_get_side (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_edge_drag_gesture_init (ShellEdgeDragGesture *self) +{ + self->side = ST_SIDE_TOP; +} + +static void +shell_edge_drag_gesture_class_init (ShellEdgeDragGestureClass *klass) +{ + ClutterGestureClass *gesture_class = CLUTTER_GESTURE_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gesture_class->should_handle_sequence = shell_edge_drag_gesture_should_handle_sequence; + gesture_class->point_began = shell_edge_drag_gesture_point_began; + gesture_class->point_moved = shell_edge_drag_gesture_point_moved; + gesture_class->point_ended = shell_edge_drag_gesture_point_ended; + + gobject_class->set_property = shell_edge_drag_gesture_set_property; + gobject_class->get_property = shell_edge_drag_gesture_get_property; + + /** + * ShellEdgeDragGesture:side: + * + * Edge that the gesture may start at. Defaults to the top edge. + */ + obj_props[PROP_SIDE] = + g_param_spec_enum ("side", NULL, NULL, + CLUTTER_TYPE_GESTURE_STATE, + ST_SIDE_TOP, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); + + /** + * ShellEdgeDragGesture::progress: + * @gesture: the #ShellEdgeDragGesture that emitted the signal + * @progress_px: the progress of the gesture in pixels in the selected direction + * + * The ::progress signal is emitted when the edge drag has moved + */ + obj_signals[PROGRESS] = + g_signal_new ("progress", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + G_TYPE_FLOAT); +} + +/** + * shell_edge_drag_gesture_set_side: + * @self: a #ShellEdgeDragGesture + * @side: the side + * + * Sets the edge of the monitor that the edge drag may start at. + */ +void +shell_edge_drag_gesture_set_side (ShellEdgeDragGesture *self, + StSide side) +{ + g_return_if_fail (SHELL_IS_EDGE_DRAG_GESTURE (self)); + g_return_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT); + + if (self->side == side) + return; + + self->side = side; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SIDE]); +} + +/** + * shell_edge_drag_gesture_get_side: + * @self: a #ShellEdgeDragGesture + * + * Gets the edge of the monitor that the edge drag may start at. + * + * Returns: the side that the edge drag may start at + */ +StSide +shell_edge_drag_gesture_get_side (ShellEdgeDragGesture *self) +{ + g_return_val_if_fail (SHELL_IS_EDGE_DRAG_GESTURE (self), ST_SIDE_TOP); + + return self->side; +} diff --git a/src/shell-edge-drag-gesture.h b/src/shell-edge-drag-gesture.h new file mode 100644 index 000000000..3acf432b9 --- /dev/null +++ b/src/shell-edge-drag-gesture.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 Jonas Dreßler + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +G_BEGIN_DECLS + +#include +#include + +#define SHELL_TYPE_EDGE_DRAG_GESTURE (shell_edge_drag_gesture_get_type ()) +G_DECLARE_FINAL_TYPE (ShellEdgeDragGesture, shell_edge_drag_gesture, + SHELL, EDGE_DRAG_GESTURE, ClutterGesture) + +void shell_edge_drag_gesture_set_side (ShellEdgeDragGesture *self, + StSide side); + +StSide shell_edge_drag_gesture_get_side (ShellEdgeDragGesture *self); + +G_END_DECLS -- 2.39.5